/**
 * 
 */
package org.eclipse.emf.db.util;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.eclipse.emf.db.DBObject;
import org.eclipse.emf.db.impl.DBObjectImpl;
import org.eclipse.emf.ecore.EAttribute;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.emf.ecore.EReference;
import org.eclipse.emf.ecore.ETypedElement;

import com.google.common.base.Predicate;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheStats;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

/**
 * @author qlefevre
 *
 */
public class LowMemoryCache<K, V extends DBObject> implements Cache<K, V> {

    private final Cache<K, V> delegate;

    private static final int DELAY=1; // minute
    private final Set<K> masterNodes=new ConcurrentSkipListSet<K>();
    private final Map<EClass, Boolean> masterNodesClasses=new ConcurrentHashMap<EClass, Boolean>();
    private final ScheduledExecutorService threadPools=Executors.newSingleThreadScheduledExecutor();

    private static final Predicate<DBObjectImpl> ISMODIFIED_PREDICATE=new Predicate<DBObjectImpl>() {
        @Override
        public boolean apply(DBObjectImpl dbObject) {
            return dbObject.dbIsModified();
        }
    };

    public LowMemoryCache(Cache<K, V> cache) {
        this.delegate=cache;
        final Runnable runnable=new Runnable() {
            @Override
            public void run() {
                for (final K k : masterNodes) {
                    threadPools.submit(new Runnable() {
                        @Override
                        public void run() {
                            V value=delegate.getIfPresent(k);
                            if (value != null) {
                                downsizeMasterNode(value);
                            }
                        }
                    });
                }
                threadPools.schedule(this, DELAY, TimeUnit.MINUTES);
            }
        };
        threadPools.submit(runnable);
    }

    @Override
    public ConcurrentMap<K, V> asMap() {
        return delegate.asMap();
    }

    @Override
    public void cleanUp() {
        delegate.cleanUp();
    }

    @Override
    public V get(K arg0, Callable<? extends V> arg1) throws ExecutionException {
        return delegate.get(arg0, arg1);
    }

    @Override
    public ImmutableMap<K, V> getAllPresent(Iterable<?> arg0) {
        return delegate.getAllPresent(arg0);
    }

    @Override
    public V getIfPresent(Object arg0) {
        return delegate.getIfPresent(arg0);
    }

    @Override
    public void invalidate(Object arg0) {
        delegate.invalidate(arg0);
    }

    @Override
    public void invalidateAll() {
        delegate.invalidateAll();
        masterNodes.clear();
    }

    @Override
    public void invalidateAll(Iterable<?> arg0) {
        delegate.invalidateAll(arg0);
    }

    @Override
    public void put(K k, V v) {
        delegate.put(k, v);
        putMasterNode(k, v);
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
            put(entry.getKey(), entry.getValue());
        }
    }

    @Override
    public long size() {
        return delegate.size();
    }

    @Override
    public CacheStats stats() {
        return delegate.stats();
    }

    private void putMasterNode(K k, V value) {
        // Check if this EClass is a master Node
        if (!masterNodesClasses.containsKey(value.eClass())) {
            masterNodesClasses.put(value.eClass(), DBModelInformationCache.getContainerReference(value.eClass()) == null);
        }
        // Add this node to master Node
        if (masterNodesClasses.get(value.eClass())) {
            masterNodes.add(k);
        }
    }

    private void downsizeMasterNode(V value) {
        List<DBObjectImpl> nodeList=getAllNodes((DBObjectImpl) value);
        if (!Iterables.any(nodeList, ISMODIFIED_PREDICATE)) {
            log(nodeList);
            for (DBObjectImpl dbObject : nodeList) {
                for (EAttribute attribute : dbObject.eClass().getEAllAttributes())
                    dbObject.internalSetLazy(attribute);
            }
        }
    }

    private void log(List<DBObjectImpl> dbObjects) {
        StringBuilder builder=new StringBuilder("=> Downsize operation on " + dbObjects.get(0)); //$NON-NLS-1$
        for (DBObjectImpl dbObject : dbObjects)
            builder.append("\n  --+ " + dbObject); //$NON-NLS-1$
        System.err.println(builder);
    }

    @SuppressWarnings("unchecked")
    private List<DBObjectImpl> getAllNodes(DBObjectImpl value) {
        List<DBObjectImpl> values=Lists.newArrayList();
        values.add(value);
        for (EReference eReference : value.eClass().getEAllReferences()) {
            Object object=value.map().get(eReference);
            if (object != null && !(object instanceof LazyLoadingInformation)) {
                if (eReference.getUpperBound() == ETypedElement.UNBOUNDED_MULTIPLICITY) {
                    for (DBObjectImpl dbObject : (List<DBObjectImpl>) object)
                        values.addAll(getAllNodes(dbObject));
                } else {
                    values.addAll(getAllNodes((DBObjectImpl) object));
                }
            }
        }
        return values;
    }

}
